<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>restc</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://shadow.elemecdn.com/gh/highlightjs/cdn-release@9.8.0/build/styles/default.min.css">
  <link rel="stylesheet" href="https://shadow.elemecdn.com/gh/codemirror/CodeMirror@5.19.0/lib/codemirror.css"></script>
  <link rel="stylesheet" href="https://shadow.elemecdn.com/gh/codemirror/CodeMirror@5.19.0/theme/material.css"></script>
  <script>
    if (!window.fetch) document.write('<script src="https://shadow.elemecdn.com/gh/github/fetch@v1.0.0/fetch.min.js"><\/script>');
  </script>
  <script src="https://shadow.elemecdn.com/npm/jinkela@1.2.18/umd.min.js"></script>
  <script src="https://shadow.elemecdn.com/gh/highlightjs/cdn-release@9.8.0/build/highlight.min.js"></script>
  <script src="https://shadow.elemecdn.com/gh/codemirror/CodeMirror@5.19.0/lib/codemirror.min.js"></script>
  <script src="https://shadow.elemecdn.com/gh/codemirror/CodeMirror@5.19.0/mode/javascript/javascript.min.js"></script>
  <script src="https://shadow.elemecdn.com/gh/codemirror/CodeMirror@5.19.0/addon/display/autorefresh.min.js"></script>
  <script src="https://shadow.elemecdn.com/npm/json-format-safely@1.2.2/index.js"></script>
  <script src="https://shadow.elemecdn.com/npm/content-disposition-attachment@0.1.1/build@module=ContentDispositionAttachment&amp;format=umd!toes5!uglifyjs!index.js"></script>
  <script extract>
    class Configuration {
      static get watchList() {
        let value = {};
        Object.defineProperty(this, 'watchList', { value, configurable: true });
        return value;
      }
      static set(key, value) {
        let oldValue = this.get(key);
        localStorage.setItem(key, JSON.stringify(value));
        if (this.watchList.hasOwnProperty(key)) {
          this.watchList[key].forEach(watcher => watcher.call(this, value, oldValue));
        }
      }
      static get(key, defaultValue = null) {
        try {
          let value = JSON.parse(localStorage.getItem(key));
          if (value === null) value = defaultValue;
          return value;
        } catch (error) {
          return defaultValue;
        }
      }
      static watch(key, callback, callImmediately = true) {
        (this.watchList.hasOwnProperty(key) ? this.watchList[key] : this.watchList[key] = []).push(callback);
        if (callImmediately) this.set(key, this.get(key));
      }
      static unwatch(key, callback) {
        if (this.watchList.hasOwnProperty(key)) {
          this.watchList[key] = this.watchList[key].filter(watcher => watcher !== callback);
        }
      }
    }

    class QueryString {
      static encode(s) {
        return encodeURIComponent(s).replace(/%20/g, '+');
      }
      static decode(s) {
        return decodeURIComponent(s.replace(/\+/g, '%20'));
      }
      static parse(queryString, preserveOrder = false) {
        let result = queryString.split('&')
          .filter(entry => entry.length)
          .map(entry => entry.split('='))
          .map(([ key, value = '' ]) => ({ key: this.decode(key), value: this.decode(value) }));
        if (preserveOrder) return result;
        return result.reduce((result, { key, value }) => Object.assign(result, { [key]: value }), {});
      }
      static stringify(parameters) {
        if (!Array.isArray(parameters)) {
          parameters = Object.keys(parameters).map(key => ({ key, value: parameters[key] }));
        }
        return parameters
          .map(({ key, value }) => [ this.encode(key), this.encode(value) ].join('='))
          .join('&');
      }
    }

    const isBodyEnabled = method => ![ 'HEAD', 'GET' ].includes(method);

    const loader = ({ element }, $promise) => {
      element.classList.add('loading');
      let restore = () => { element.classList.remove('loading'); };
      return $promise.then(data => {
        restore();
        return data;
      }, e => {
        restore();
        throw e;
      });
    }

    class HighlightBlock extends Jinkela {
      get template() {
        return `<pre><code class="http" ref="body"></code></pre>`;
      }
      update(code, ...elements) {
        this.body.textContent = code;
        hljs.highlightBlock(this.body);
        for (let element of elements) {
          if (element instanceof Jinkela) element.to(this.body);
        }
      }
      get styleSheet() {
        return `
          :scope {
            font-size: 12px;
            line-height: 1.5;
            & > code {
              margin: 0 8px;
              padding: 20px 32px;
              background: transparent;
              overflow: visible;
            }
          }
        `;
      }
    }

    class RequestView extends Jinkela {
      constructor(...args) {
        super(...args);
      }
      init() {
        this.block = new HighlightBlock().to(this);
      }
      update({ method, path, queryString, body, headers }) {
        let uri = path;
        if (queryString) uri += '?' + queryString;
        headers = Object.keys(headers)
          .map(key => ({ key, value: headers[key] }))
          .map(({ key, value }) => ({ key, values: Array.isArray(value) ? value : [ value ] }))
          .reduce((result, { key, values }) => [ ...result, ...values.map(value => ({ key, value })) ], []);
        let request = [
          [ method, uri, 'HTTP/1.1' ].join(' '),
          ...headers.map(({ key, value }) => [ key, value ].join(': '))
        ].join('\n');
        if (isBodyEnabled(method) && body) request = [ request, body ].join('\n\n');
        this.block.update(request);
      }
    }

    class ResponseViewImage extends Jinkela {
      beforeParse(params) {
        if (!params.filename) {
          params.filename = '';
        }
      }
      get template() {
        return `
          <a download="{filename}" href="{url}">
            <img src="{url}"/>
          </a>
        `;
      }
      get styleSheet() {
        return `
          :scope {
            text-decoration: none;
          }
        `;
      }
    }

    class ResponseViewDownloadLink extends Jinkela {
      beforeParse(params) {
        if (!params.filename) {
          params.filename = '';
        }
      }
      get template() {
        return `
          <a download="{filename}" href="{url}">
            <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1022.955 522.57c0 100.193-81.516 181.699-181.719 181.699H655.6c-11.298 0-20.467-9.169-20.467-20.466 0-11.308 9.17-20.466 20.467-20.466h185.637c77.628 0 140.787-63.148 140.787-140.766 0-77.424-62.841-140.449-140.203-140.766-.42.03-.819.05-1.218.061-5.945.143-11.686-2.292-15.687-6.703a20.455 20.455 0 0 1-5.168-16.25c1.33-10.806 1.944-19.76 1.944-28.192 0-60.764-23.658-117.885-66.617-160.833-42.969-42.968-100.09-66.617-160.843-66.617-47.369 0-92.742 14.45-131.208 41.782-37.617 26.739-65.953 63.7-81.926 106.884a20.5 20.5 0 0 1-14.828 12.894 20.492 20.492 0 0 1-18.86-5.547c-19.289-19.33-44.943-29.972-72.245-29.972-56.323 0-102.146 45.813-102.146 102.126 0 .317.04.982.092 1.627.061.92.122 1.831.153 2.763a20.466 20.466 0 0 1-15.002 20.455c-32.356 8.934-61.541 28.55-82.181 55.218-21.305 27.517-32.572 60.508-32.572 95.413 0 86.244 70.188 156.423 156.443 156.423h169.981c11.298 0 20.466 9.158 20.466 20.466 0 11.297-9.168 20.466-20.466 20.466H199.951c-108.829 0-197.375-88.536-197.375-197.355 0-44.053 14.224-85.712 41.126-120.474 22.81-29.46 53.898-52.086 88.71-64.816 5.066-74.323 67.15-133.245 142.752-133.245 28.386 0 55.504 8.218 78.651 23.526 19.658-39.868 48.843-74.169 85.498-100.212 45.434-32.296 99.004-49.354 154.918-49.354 71.693 0 139.088 27.916 189.782 78.6 50.695 50.695 78.61 118.09 78.61 189.782 0 3.705-.102 7.47-.296 11.37 90.307 10.478 160.628 87.42 160.628 180.48z"/><path d="M629.259 820.711L527.235 922.724c-3.99 4.002-9.23 5.997-14.47 5.997s-10.478-1.995-14.47-5.997L396.273 820.711c-7.992-7.992-7.992-20.947 0-28.94s20.947-8.001 28.94 0l67.087 67.079v-358.7c0-11.297 9.159-20.466 20.466-20.466 11.308 0 20.467 9.169 20.467 20.466v358.7l67.088-67.078c7.992-8.002 20.947-7.992 28.939 0s7.992 20.947 0 28.939z"/></svg>
            Download <span>{filename}</span>
          </a>
        `;
      }
      get styleSheet() {
        return `
          :scope {
            --r: 76;
            --g: 82;
            --b: 100;
            --a: .9;
            background: rgba(var(--r), var(--g), var(--b), var(--a));
            border-radius: 4px;
            color: #fff;
            display: inline-block;
            fill: #fff;
            font-family: sans-serif;
            padding: 10px 15px;
            text-decoration: none;
            transition: background .2s ease;
            white-space: nowrap;
            > svg {
              display: inline;
              vertical-align: middle;
            }
            &:hover {
              --a: .8;
            }
            &:active {
              --a: 1;
            }
          }
        `;
      }
    }

    class ResponseView extends Jinkela {
      constructor(...args) {
        super(...args);
      }
      init() {
        this.block = new HighlightBlock().to(this);
      }
      update($response) {
        return Promise.resolve($response).then(response => {
          let status = [ 'HTTP/1.1', response.status, response.statusText ].join(' ');
          let headers = [ ...response.headers.entries() ].map(this.stringifyHeader).join('\n');
          let contentType = response.headers.get('Content-Type');
          let json = /json/.test(contentType);
          let javascript = /javascript/.test(contentType);
          let binary = !(/text/.test(contentType) || json || javascript);
          let image = /image/.test(contentType);
          let contentDisposition = response.headers.get('Content-Disposition') || '';
          try {
            contentDisposition = ContentDispositionAttachment.parse(contentDisposition);
          } catch (error) {
            contentDisposition = { attachment: false };
          }
          let { attachment, filename } = contentDisposition;
          let $body;
          if (image) {
            $body = response.blob().then(URL.createObjectURL).then(url => {
              return { elements: [ new ResponseViewImage({ url, filename }) ] };
            });
          } else if (binary || attachment) {
            $body = response.blob().then(URL.createObjectURL).then(url => {
              return { elements: [ new ResponseViewDownloadLink({ url, filename }) ] };
            });
          } else {
            $body = response.text().then(result => {
              let body = result;
              if (json) {
                try {
                  body = jsonFormatSafely(body);
                } catch (e) { /* ignore */ }
              }
              return { body };
            });
          }
          $body.then(({ body, elements = [] }) => {
            this.block.update([ status, headers, '', body ].join('\n'), ...elements);
          })
        }).catch(({ message }) => {
          this.block.update(message);
        });
      }
      stringifyHeader([ key, value ]) {
        key = key.replace(/(?:^|-)./g, $0 => $0.toUpperCase());
        return [ key, value ].join(': ');
      }
      get styleSheet() {
        return `
          :scope {
            height: 100%;
            overflow: auto;
            &.loading {
              position: relative;
              &:after {
                position: absolute;
                left: 3px;
                right: 3px;
                top: 3px;
                bottom: 3px;
                content: '';
                background: rgba(255, 255, 255, 0.8) url('') 50% 50% no-repeat;
                backdrop-filter: blur(3px);
                -webkit-backdrop-filter: blur(3px);
                cursor: progress;
              }
            }
          }
        `;
      }
    }

    class ResultView extends Jinkela {
      get padding() { return 0; }
      get btnWidth() { return 120; }
      get template() {
        return `
          <dl>
            <dt on-click="{click}">
              <span ref="underline"></span>
              <a href="JavaScript:" data-binding="ResponseView" data-nth="0">Response</a>
              <a href="JavaScript:" data-binding="RequestView" data-nth="1">Request</a>
            </dt>
            <dd>
              <meta ref="list" />
            </dd>
          </dl>
        `;
      }
      click({ target }) {
        let binding = target.dataset.binding;
        if (!binding) return;
        this.underline.style.left = this.padding + target.dataset.nth * this.btnWidth + 'px';
        this.list.forEach(item => {
          item.element.style.display = item.constructor.name === binding ? 'block' : 'none';
        });
      }
      get requestView() {
        let value = new RequestView();
        Object.defineProperty(this, 'requestView', { value, configurable: true });
        return value;
      }
      get responseView() {
        let value = new ResponseView();
        Object.defineProperty(this, 'responseView', { value, configurable: true });
        return value;
      }
      init() {
        this.list = [ this.requestView, this.responseView ];
        this.element.querySelector('a').click();
      }
      set waiting(value) {
        if (value) {
          this.element.classList.add('waiting');
        } else {
          this.element.classList.remove('waiting');
        }
      }
      get waiting() { return this.element.classList.has('waiting'); }
      update(request, $response) {
        this.waiting = false;
        this.requestView.update(request);
        return loader(this, this.responseView.update($response));
      }
      get styleSheet() {
        return `
          :scope {
            position: relative;
            height: 100%;
            box-sizing: border-box;
            margin: 0;
            display: flex;
            flex-direction: column;
            > dt {
              position: relative;
              border-bottom: 1px solid #e4e4ec;
              padding: 0 ${this.padding}px;
              margin: 0;
              font-size: 0;
              > a {
                position: relative;
                z-index: 1;
                text-decoration: none;
                display: inline-block;
                box-sizing: border-box;
                padding: .5em 1em;
                font-size: 16px;
                width: ${this.btnWidth}px;
                text-align: center;
                font-weight: 600;
                color: #5d6576;
                &:hover { opacity: .8; }
              }
              > span {
                transition: left 200ms ease, right 200ms ease;
                position: absolute;
                top: 0;
                bottom: -1px;
                font-size: 16px;
                width: ${this.btnWidth}px;
                border-bottom: 2px solid #2d2f3b;
                background: #f1f1f6;
              }
            }
            > dd {
              flex: 1;
              overflow: auto;
              margin: 0;
            }
            &.loading:after {
              position: absolute;
              left: 0;
              right: 0;
              top: 0;
              bottom: 0;
              content: '';
              background: rgba(248, 248, 250, 0.8) url('') 50% 50% no-repeat;
              cursor: progress;
              z-index: 1000;
            }
            &.waiting:after {
              position: absolute;
              left: 0;
              top: 0;
              right: 0;
              bottom: 0;
              display: flex;
              align-items: center;
              justify-content: center;
              content: 'Click “Send” to send the request';
              font-weight: 500;
              font-size: 1.2em;
              background: rgba(248, 248, 250, 0.8);
              z-index: 1000;
            }
          }
        `;
      }
    }

    class SideBarInput extends Jinkela {
      get template() {
        return `
          <div>
            <input ref="input" placeholder="{placeholder}" />
          </div>
        `;
      }
      get placeholder() { return ''; }
      set value(value) { this.input.value = value; }
      get value() { return this.input.value; }
      select() {
        this.input.select();
      }
      get styleSheet() {
        return `
          :scope {
            padding: 0 1em;
            display: flex;
            box-sizing: border-box;
            color: #dbdfeb;
            background: #3f4555;
            line-height: 40px;
            border: 0 none;
            border-radius: 8px;
            > input {
              margin: 0;
              padding: 0;
              flex: 1;
              border: 0 none;
              color: inherit;
              background: transparent;
              font: inherit;
              vertical-align: middle;
              outline: none;
              &::-webkit-input-placeholder {
                color: #5d6577;
              }
            }
          }
        `;
      }
    }

    class SideBarMethodSelect extends Jinkela {
      get template() {
        return `
          <select>
            <option>GET</option>
            <option>POST</option>
            <option>PUT</option>
            <option>PATCH</option>
            <option>DELETE</option>
          </select>
        `;
      }
      set value(value) {
        this.element.value = value;
      }
      get value() {
        return this.element.value;
      }
      get styleSheet() {
        return `
          :scope {
            padding: 0 1.5em 0 1em;
            color: #dbdfeb;
            background: #3f4555;
            font: inherit;
            min-width: 40px;
            max-width: 100px;
            height: 40px;
            border: 0 none;
            border-radius: 8px;
            outline: none;
            -webkit-appearance: none;
            -moz-appearance: none;
            background-image: url('');
            background-position: 90% 50%;
            background-repeat: no-repeat;
            background-size: 16px;
            cursor: pointer;
            &:hover {
              color: #fff;
            }
          }
        `;
      }
    }

    class SideBarQueryStringInput extends SideBarInput {
      get styleSheet() {
        return `
          :scope {
            flex: 1;
            &:before {
              color: #5d6577;
            }
          }
        `;
      }
    }

    class SideBarSendButton extends Jinkela {
      get template() { return '<button>Send</button>'; }
      get styleSheet() {
        return `
          :scope {
            padding: 0 1em;
            height: 40px;
            color: #dbdfeb;
            background: transparent;
            font: inherit;
            border: 2px solid #959cad;
            border-radius: 4px;
            outline: none;
            cursor: pointer;
            transition: .15s background;
            &:hover {
              color: #fff;
              border-color: #fff;
            }
          }
        `;
      }
    }

    class SideBarMethod extends Jinkela {
      get template() {
        return `
          <div>
            <jkl-side-bar-method-select ref="methodSelect" on-change="{methodChange}"></jkl-side-bar-method-select>
            <jkl-side-bar-query-string-input
              ref="queryStringInput"
              on-input="{queryStringChange}"
              placeholder="URL or query string"></jkl-side-bar-query-string-input>
            <jkl-side-bar-send-button></jkl-side-bar-send-button>
          </div>
        `;
      }
      set method(value) { this.methodSelect.value = value; }
      get method() { return this.methodSelect.value; }
      set queryString(value) { this.queryStringInput.value = value; }
      get queryString() {
        let value = this.queryStringInput.value;
        if (value.match(new RegExp(`^https?://`)) || value.match(new RegExp(`^/`))) {
          let [, url = value, query] = value.match(/(.*)\?(.*)/) || []
          value = query ? query : ``;
          if (url.match(new RegExp(`^//`))) {
            url = `${location.protocol}${url}`
          } else if(url.match(new RegExp(`^/`))) {
            url = `${location.origin}${url}`
          }
          this.url = url
        }
        return value;
      }
      methodChange() {
        this.element.dispatchEvent(new CustomEvent('methodchange', { bubbles: true, detail: this.method }));
      }
      queryStringChange() {
        this.element.dispatchEvent(new CustomEvent('querystringchange', { bubbles: true, detail: this.queryString }));
      }
      get styleSheet() {
        return `
          :scope {
            margin: -.25em;
            display: flex;
            & > * {
              margin: .25em;
            }
          }
        `;
      }
    }

    class SideBarKeyValuePairCheckBox extends Jinkela {
      get template() {
        return `
          <input type="checkbox" />
        `;
      }
      set checked(value) {
        this.element.checked = value;
      }
      get checked() {
        return this.element.checked;
      }
      get styleSheet() {
        return `
          :scope {
            margin: 0 8px 0 10px;
            width: 18px;
            height: 18px;
            -webkit-appearance: none;
            -moz-appearance: none;
            background: #2d2f3b;
            border-radius: 4px;
            cursor: pointer;
            outline: none;
            &:hover {
              background: #262c39;
            }
            &:checked {
              background-position: center center;
              background-repeat: no-repeat;
              background-image: url('');
            }
          }
        `;
      }
    }

    class SideBarKeyValuePairInput extends SideBarInput {
      get styleSheet() {
        return `
          :scope {
            padding: 4px 6px 4px 0;
            flex: 1;
            border-radius: 0;
            font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
            line-height: 32px;
          }
        `;
      }
    }

    class SideBarKeyValuePair extends Jinkela {
      get template() {
        return `
          <div>
            <jkl-side-bar-key-value-pair-check-box
              ref="enabledCheckBox"
              on-click="{emitChange}">
            </jkl-side-bar-key-value-pair-check-box>
            <jkl-side-bar-key-value-pair-input
              placeholder="key"
              ref="keyInput"
              on-input="{emitChange}">
            </jkl-side-bar-key-value-pair-input>
            <jkl-side-bar-key-value-pair-input
              placeholder="value"
              ref="valueInput"
              on-input="{emitChange}">
            </jkl-side-bar-key-value-pair-input>
          </div>
        `;
      }
      set enabled(value) { this.enabledCheckBox.checked = value; }
      get enabled() { return this.enabledCheckBox.checked; }
      set key(value) { this.keyInput.value = value; }
      get key() { return this.keyInput.value; }
      set value(value) { this.valueInput.value = value; }
      get value() { return this.valueInput.value; }
      select() {
        this.keyInput.select();
      }
      emitChange() {
        this.element.dispatchEvent(new CustomEvent('pairchange', { bubbles: true }));
      }
      get styleSheet() {
        return `
          :scope {
            margin-bottom: 1px;
            display: flex;
            align-items: center;
            background: #3f4555;
            font-size: 12px;
          }
        `;
      }
    }

    class SideBarKeyValueAdd extends Jinkela {
      get template() {
        return `
          <div>
            <span ref="children"></span>
          </div>
        `;
      }
      get styleSheet() {
        return `
          :scope {
            padding: 0 11px 0 35px;
            display: flex;
            align-items: center;
            height: 40px;
            color: #5d6577;
            background: #3f4555 url('') 12px 50% no-repeat;
            font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
            font-size: 12px;
            cursor: pointer;
            &:hover {
              color: #fff;
            }
          }
        `;
      }
    }

    class SideBarKeyValueCollection extends Jinkela {
      get template() {
        return `
          <div>
            <div ref="list"></div>
            <div>
              <jkl-side-bar-key-value-add on-click="{add}">{text}</jkl-side-bar-key-value-add>
            </div>
          </div>
        `;
      }
      get text() { return 'add'; }
      fromJSON(json) {
        while (this.list.firstChild) this.list.firstChild.remove();
        this.$collection = SideBarKeyValuePair
          .from(json.map(({ enabled = true, key, value }) => ({ enabled, key, value })))
          .to(this.list);
      }
      toJSON() {
        return (this.$collection || [])
          .map(({ enabled, key, value }) => ({ enabled, key, value }));
      }
      set collection(value) { this.fromJSON(value); }
      get collection() {
        return this.toJSON()
          .filter(({ enabled }) => enabled)
          .map(({ key, value }) => ({ key, value }));
      }
      add() {
        let item = new SideBarKeyValuePair({ enabled: true, key: 'new item' }).to(this.list);
        item.select();
        (this.$collection || (this.$collection = [])).push(item);
        this.element.dispatchEvent(new CustomEvent('collectionchange', { bubbles: true }));
      }
      get styleSheet() {
        return `
          :scope {
            > * {
              border-radius: 4px;
              overflow: hidden;
            }
          }
        `;
      }
    }

    class SideBarBodyEditor extends Jinkela {
      get template() {
        return `
          <div>
            <h3>Body</h3>
          </div>
        `;
      }
      init() {
        this.cm = CodeMirror(this.element, {
          mode: 'application/json',
          theme: 'material'
        });
      }
      didMount() {
        this.refresh();
      }
      set value(value) { this.cm.setValue(value); }
      get value() { return this.cm.getValue(); }
      refresh() {
        setTimeout(() => this.cm.refresh(), 0);
      }
      get styleSheet() {
        return `
          :scope {
            .CodeMirror {
              padding: 1em;
              height: 10em;
              border-radius: 8px;
              background: #3f4555;
              font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
              line-height: 1.5;
            }
          }
        `;
      }
    }

    class SideBarHeaderCollection extends SideBarKeyValueCollection {
      get value() {
        return this.collection
          .reduce((result, { key, value }) => {
            (result[key] || (result[key] = [])).push(value);
            return result;
          }, {});
      }
    }
    class SideBar extends Jinkela {
      get template() {
        return `
          <form on-submit="{submit}">
            <jkl-side-bar-method
              ref="methodComponent"
              on-methodchange="{methodChange}"
              on-querystringchange="{syncQueryParameters}">
            </jkl-side-bar-method>
            <h3>Query Parameters</h3>
            <jkl-side-bar-key-value-collection
              ref="queryParameterCollection"
              text="add query parameter"
              on-collectionchange="{syncQueryString}"
              on-pairchange="{syncQueryString}">
            </jkl-side-bar-key-value-collection>
            <jkl-side-bar-body-editor ref="bodyEditor"></jkl-side-bar-body-editor>
            <h3>Headers</h3>
            <jkl-side-bar-header-collection
              ref="headerCollection"
              text="add header">
          </form>
        `;
      }
      set method(value) {
        this.methodComponent.method = value;
        this.methodChange();
      }
      get method() { return this.methodComponent.method; }
      set queryString(value) {
        this.methodComponent.queryString = value;
        this.syncQueryParameters();
      }
      get queryString() { return this.methodComponent.queryString; }
      set queryParameters(value) {
        this.queryParameterCollection.collection = value;
        this.syncQueryString();
      }
      get queryParameters() { return this.queryParameterCollection.collection; }
      set body(value) { return this.bodyEditor.value = value; }
      get body() { return this.bodyEditor.value; }
      set headers(value) { this.headerCollection.collection = value; }
      get headers() { return this.headerCollection.value; }
      fromJSON(json) {
        let { method, queryParameters, body, headers, uri } = json;
        this.uri = uri;
        if (method) this.method = method.toUpperCase();
        if (queryParameters) {
          this.queryParameters = queryParameters;
        } else {
          this.queryString =  location.search.slice(1);
        }
        if (body) this.body = body;
        if (headers) this.headerCollection.fromJSON(headers);
      }
      toJSON() {
        let { method, body, uri } = this;
        let queryParameters = this.queryParameterCollection.toJSON();
        let headers = this.headerCollection.toJSON();
        return { method, queryParameters, body, headers, uri };
      }
      set isBodyVisible(value) {
        if (value) {
          this.bodyEditor.element.style.display = 'block';
          this.bodyEditor.refresh();
        } else {
          this.bodyEditor.element.style.display = 'none';
        }
      }
      get isBodyVisible() { return this.bodyEditor.element.style.display !== 'none'; }
      init() {
        let { method, queryParameters = [], body, headers = [], uri, url} = QueryString.parse(location.hash.replace(/^#!?/, ''));
        uri = uri || url || `${location.protocol}//${location.host}${location.pathname}`
        try { headers = JSON.parse(headers); } catch (error) { /* ignore */ }
        try { queryParameters = JSON.parse(queryParameters); } catch (error) { /* ignore */ }
        this.fromJSON({ method, queryParameters, body, headers, uri });
        this.methodChange();
      }
      submit(event) {
        event.preventDefault();
        let { method, queryParameters, body, headers, uri } = this.toJSON();
        if (queryParameters) queryParameters = JSON.stringify(queryParameters);
        if (headers) headers = JSON.stringify(headers);
        let hash = QueryString.stringify({ method, queryParameters, body, headers, uri });
        if (hash) hash = '#!' + hash;
        if (location.hash !== hash) {
          location.hash = hash;
        } else {
          dispatchEvent(new Event('hashchange'));
        }
      }
      methodChange() {
        this.isBodyVisible = isBodyEnabled(this.method);
      }
      syncQueryParameters() {
        let { queryString } = this;
        this.uri = this.methodComponent.url || this.uri
        let queryParameters = QueryString.parse(queryString, true);
        this.queryParameterCollection.collection = queryParameters;
      }
      syncQueryString() {
        let { collection } = this.queryParameterCollection;
        let queryString = QueryString.stringify(collection);
        this.methodComponent.queryString = `${this.uri}${queryString ? `?${queryString}` : ``}`;
      }
      get styleSheet() {
        return `
          :scope {
            padding: 40px;
            box-sizing: border-box;
            color: #fff;
            background: #2d2f3b;
            transition: transform 1s;
            height: 100%;
            box-sizing: border-box;
            overflow: auto;
            h3 {
              margin-left: -40px;
              margin-right: -40px;
              padding: 20px 40px;
              border-bottom: 1px solid #373b48;
              font-size: 17px;
              font-weight: 600;
            }
          }
        `;
      }
    }

    class Main extends Jinkela {
      get template() {
        return `
          <div class="gMain">
            <section>
              <jkl-result-view ref="resultView"></jkl-result-view>
            </section>
            <aside>
              <jkl-side-bar ref="sideBar"></jkl-side-bar>
            </aside>
          </div>
        `;
      }
      init() {
        if (this.sideBar.method === 'GET' || this.sendByUser) {
          this.sendRequest();
        } else {
          this.resultView.waiting = true;
        }
        Configuration.watch('settings.sidebar.collapse', value => {
          if (value) {
            this.element.classList.add('sidebar-collapse');
          } else {
            this.element.classList.remove('sidebar-collapse');
          }
        });
      }
      sendRequest() {
        let { method, queryString, body, headers, uri } = this.sideBar;
        let path = new URL(uri).pathname
        let defaultHeaders = {
          Accept: 'application/json, */*',
          'Content-Type': 'application/json'
        };
        headers = Object.assign({}, defaultHeaders, headers);
        if (queryString) uri += '?' + queryString;
        let options = { method, headers, credentials: 'include' };
        if (this.sideBar.isBodyVisible) options.body = body;
        let $response = fetch(uri, options);
        this.resultView.update({ method, path, queryString, body, headers }, $response);
        // sync title
        document.title = [ method, uri ].join(' ');
      }
      get styleSheet() {
        return `
          :scope {
            position: relative;
            flex: 1;
            width: 100%;
            min-height: 0;
            > section, > aside {
              position: absolute;
              top: 0;
              bottom: 0;
            }
            > section {
              left: 0;
              width: 57%;
              transition: width .01s .7s;
            }
            > aside {
              left: 57%;
              right: 0;
              transition: transform .7s;
            }
            &.sidebar-collapse {
              > section {
                width: 100%;
                transition: none;
              }
              > aside {
                transform: translateX(100%);
              }
            }
          }
        `;
      }
    }

    class Frame extends Jinkela {
      init() {
        let { sendByUser } = this;
        new Main({ sendByUser }).to(this);
      }
      get styleSheet() {
        return `
          html {
            height: 100%;
            overflow: hidden;
          }
          @media screen and (max-width: 750px) {
            html {
              overflow: scroll;
            }
            html body .gMain {
              display: flex;
              flex-wrap: wrap;
            }
            html body .gMain > section,
            html body .gMain > aside {
              width: 100%;
              position: initial;
            }
            html body .gMain > section {
              order: 2;
            }
            html body .gMain > aside {
              order: 1;
            }
            html body .gMain > aside > form {
              overflow: hidden;
              padding: 20px;
            }
          }
          body {
            margin: 0;
            height: 100%;
            background: #fff;
            color: #4c555a;
            font-family:
              "Alright Sans LP", "Avenir Next", "Helvetica Neue", Helvetica, Arial, "PingFang SC",
              "Source Han Sans SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi MicroHei", sans-serif;
            font-size: 14px;
            -webkit-font-smoothing: antialiased;
            line-height: 26px;
          }
          pre, code {
            font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
          }
          :scope {
            display: flex;
            flex-direction: column;
            width: 100%;
            height: 100%;
          }
        `;
      }
    }

    let frame = new Frame();
    addEventListener('hashchange', () => frame = new Frame({ sendByUser: true }).renderWith(frame));
    addEventListener('DOMContentLoaded', () => frame.to(document.body));
  </script>
</head>
</html>
